"""
Swagger API client for discovering and fetching API documentation
"""
import requests
import logging
import time
from typing import Dict, List, Optional, Any, Tuple
from urllib.parse import urljoin
import json

from ..config import InCloudConfig, AuthConfig
from ..logging_config import log_api_request, log_swagger_discovery

logger = logging.getLogger(__name__)


class SwaggerClient:
    """Client for interacting with Swagger API documentation"""
    
    def __init__(self, config: InCloudConfig):
        self.config = config
        self.base_url = config.swagger.base_url
        self.timeout = config.swagger.timeout
        self.session = requests.Session()
        
        # Setup authentication
        if config.auth:
            self._setup_auth(config.auth)
    
    def _setup_auth(self, auth_config: AuthConfig):
        """Setup authentication for requests"""
        if auth_config.type == "bearer" and auth_config.token:
            self.session.headers.update({
                "Authorization": f"Bearer {auth_config.token}"
            })
        elif auth_config.type == "basic" and auth_config.username and auth_config.password:
            self.session.auth = (auth_config.username, auth_config.password)
        elif auth_config.type == "api_key" and auth_config.api_key:
            header_name = auth_config.api_key_header or "X-API-Key"
            self.session.headers.update({
                header_name: auth_config.api_key
            })
    
    def get_swagger_resources(self) -> List[Dict[str, Any]]:
        """
        Get list of available Swagger resources from /swagger-resources endpoint

        Returns:
            List of service resources with name, url, and version info
        """
        try:
            url = urljoin(self.base_url, "/swagger-resources")
            logger.info(f"Fetching Swagger resources from: {url}")

            start_time = time.time()
            response = self.session.get(url, timeout=self.timeout)
            response_time = time.time() - start_time

            log_api_request(url, "GET", response.status_code, response_time)
            response.raise_for_status()

            resources = response.json()
            logger.info(f"Found {len(resources)} Swagger resources")

            # Log each resource for debugging
            for i, resource in enumerate(resources, 1):
                name = resource.get('name', 'Unknown')
                url_path = resource.get('url', 'Unknown')
                version = resource.get('swaggerVersion', 'Unknown')
                logger.debug(f"  Resource {i}: {name} -> {url_path} (version: {version})")

            return resources

        except Exception as e:
            log_api_request(url, "GET", error=str(e))
            logger.error(f"Failed to get Swagger resources from {url}: {e}")
            raise
    
    def get_service_api_docs(self, service_name: str) -> Dict[str, Any]:
        """
        Get API documentation for a specific service

        Args:
            service_name: Name of the service (e.g., 'icompute', 'istorage')

        Returns:
            Swagger API documentation as dict
        """
        logger.info(f"Attempting to fetch API docs for service: {service_name}")

        # Try different URL patterns for API docs
        possible_urls = [
            f"/{service_name}/v2/api-docs",
            f"/v2/api-docs?group={service_name}",
            f"/{service_name}/swagger.json",
            f"/swagger-ui/{service_name}/v2/api-docs",
            f"/api/{service_name}/v2/api-docs"
        ]

        urls_tried = []
        last_error = None

        for i, path in enumerate(possible_urls, 1):
            try:
                url = urljoin(self.base_url, path)
                urls_tried.append(url)

                logger.debug(f"Trying URL {i}/{len(possible_urls)}: {url}")

                start_time = time.time()
                response = self.session.get(url, timeout=self.timeout)
                response_time = time.time() - start_time

                log_api_request(url, "GET", response.status_code, response_time)

                if response.status_code == 200:
                    try:
                        api_docs = response.json()
                        log_swagger_discovery(service_name, urls_tried, url)
                        logger.info(f"Successfully fetched API docs for {service_name} from {path}")
                        logger.debug(f"API docs info - Title: {api_docs.get('info', {}).get('title', 'N/A')}, "
                                   f"Version: {api_docs.get('info', {}).get('version', 'N/A')}, "
                                   f"Paths: {len(api_docs.get('paths', {}))}")
                        return api_docs
                    except json.JSONDecodeError as je:
                        last_error = f"Invalid JSON response from {url}: {je}"
                        logger.warning(last_error)
                        continue
                else:
                    last_error = f"HTTP {response.status_code} from {url}"
                    logger.debug(last_error)
                    continue

            except requests.exceptions.Timeout as te:
                last_error = f"Timeout accessing {url}: {te}"
                logger.warning(last_error)
                log_api_request(url, "GET", error=str(te))
                continue
            except requests.exceptions.ConnectionError as ce:
                last_error = f"Connection error accessing {url}: {ce}"
                logger.warning(last_error)
                log_api_request(url, "GET", error=str(ce))
                continue
            except requests.exceptions.RequestException as re:
                last_error = f"Request error accessing {url}: {re}"
                logger.warning(last_error)
                log_api_request(url, "GET", error=str(re))
                continue

        # All attempts failed
        error_msg = f"Could not find API docs for service {service_name}"
        if last_error:
            error_msg += f". Last error: {last_error}"

        log_swagger_discovery(service_name, urls_tried, error=error_msg)
        raise Exception(error_msg)
    
    def get_available_services(self) -> List[str]:
        """
        Get list of available service names
        
        Returns:
            List of service names
        """
        try:
            resources = self.get_swagger_resources()
            services = []
            
            for resource in resources:
                # Extract service name from resource
                name = resource.get('name', '')
                if name and name not in services:
                    services.append(name)
            
            return services
            
        except Exception as e:
            logger.error(f"Failed to get available services: {e}")
            return []
    
    def get_service_apis(self, service_name: str) -> List[Dict[str, Any]]:
        """
        Get list of APIs for a specific service

        Args:
            service_name: Name of the service

        Returns:
            List of API information with method, path, summary, etc.
        """
        try:
            logger.info(f"Getting API docs for service: {service_name}")
            api_docs = self.get_service_api_docs(service_name)

            # Cache the API docs for reference resolution
            self._current_api_docs = api_docs

            apis = []

            paths = api_docs.get('paths', {})
            logger.info(f"Found {len(paths)} paths in Swagger docs for {service_name}")

            for path, methods in paths.items():
                logger.debug(f"Processing path: {path}")
                for method, details in methods.items():
                    if method.upper() in ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']:
                        api_info = {
                            'method': method.upper(),
                            'path': path,
                            'summary': details.get('summary', ''),
                            'description': details.get('description', ''),
                            'operationId': details.get('operationId', ''),
                            'tags': details.get('tags', []),
                            'parameters': details.get('parameters', []),
                            'responses': details.get('responses', {}),
                            'requestBody': details.get('requestBody', {})
                        }
                        apis.append(api_info)
                        logger.debug(f"  Added API: {method.upper()} {path}")

            logger.info(f"Found {len(apis)} APIs for service {service_name}")
            return apis

        except Exception as e:
            logger.error(f"Failed to get APIs for {service_name}: {e}")
            return []
    
    def get_api_details(self, service_name: str, method: str, path: str) -> Optional[Dict[str, Any]]:
        """
        Get detailed information for a specific API

        Args:
            service_name: Name of the service
            method: HTTP method
            path: API path

        Returns:
            Detailed API information or None if not found
        """
        try:
            # Ensure we have the API docs cached for reference resolution
            if not hasattr(self, '_current_api_docs') or not self._current_api_docs:
                api_docs = self.get_service_api_docs(service_name)
                self._current_api_docs = api_docs

            apis = self.get_service_apis(service_name)

            for api in apis:
                if api['method'] == method.upper() and api['path'] == path:
                    return api

            logger.warning(f"API not found: {method} {path} in service {service_name}")
            return None

        except Exception as e:
            logger.error(f"Failed to get API details: {e}")
            return None
    
    def _resolve_ref(self, ref_path: str, api_docs: Dict[str, Any]) -> Dict[str, Any]:
        """
        Resolve a $ref reference to its actual schema definition

        Args:
            ref_path: Reference path like "#/definitions/VirtualVolumeCreateVo"
            api_docs: Complete API documentation containing definitions

        Returns:
            Resolved schema definition
        """
        if not ref_path.startswith('#/'):
            logger.warning(f"Unsupported ref format: {ref_path}")
            return {}

        # Remove '#/' prefix and split path
        path_parts = ref_path[2:].split('/')

        # Navigate through the API docs to find the referenced schema
        current = api_docs
        for part in path_parts:
            if isinstance(current, dict) and part in current:
                current = current[part]
            else:
                logger.warning(f"Reference path not found: {ref_path}")
                return {}

        return current if isinstance(current, dict) else {}

    def _extract_properties_from_schema(self, schema: Dict[str, Any], api_docs: Dict[str, Any],
                                       visited_refs: set = None) -> List[Dict[str, Any]]:
        """
        Extract properties from a schema, resolving $ref references

        Args:
            schema: Schema definition
            api_docs: Complete API documentation
            visited_refs: Set of visited references to prevent circular references

        Returns:
            List of parameter information
        """
        if visited_refs is None:
            visited_refs = set()

        parameters = []

        # Handle $ref reference
        if '$ref' in schema:
            ref_path = schema['$ref']
            if ref_path in visited_refs:
                logger.warning(f"Circular reference detected: {ref_path}")
                return parameters

            visited_refs.add(ref_path)
            resolved_schema = self._resolve_ref(ref_path, api_docs)
            if resolved_schema:
                parameters.extend(self._extract_properties_from_schema(
                    resolved_schema, api_docs, visited_refs.copy()
                ))
            visited_refs.discard(ref_path)
            return parameters

        # Handle direct properties
        if 'properties' in schema:
            required_fields = schema.get('required', [])
            for prop_name, prop_details in schema['properties'].items():
                # Handle nested $ref in properties
                if '$ref' in prop_details:
                    ref_path = prop_details['$ref']
                    if ref_path not in visited_refs:
                        visited_refs.add(ref_path)
                        resolved_prop = self._resolve_ref(ref_path, api_docs)
                        if resolved_prop:
                            # For nested objects, we might want to flatten them or handle them differently
                            # For now, we'll create a parameter with object type and include the resolved schema
                            param_info = {
                                'name': prop_name,
                                'type': 'object',
                                'in': 'body',
                                'required': prop_name in required_fields,
                                'description': prop_details.get('description', resolved_prop.get('description', '')),
                                'schema': resolved_prop
                            }
                            parameters.append(param_info)

                            # Also extract nested properties if they exist
                            nested_params = self._extract_properties_from_schema(
                                resolved_prop, api_docs, visited_refs.copy()
                            )
                            # Prefix nested parameter names to avoid conflicts
                            for nested_param in nested_params:
                                nested_param['name'] = f"{prop_name}.{nested_param['name']}"
                                nested_param['description'] = f"[{prop_name}] {nested_param['description']}"
                            parameters.extend(nested_params)
                        visited_refs.discard(ref_path)
                else:
                    # Regular property
                    param_info = {
                        'name': prop_name,
                        'type': prop_details.get('type', 'string'),
                        'in': 'body',
                        'required': prop_name in required_fields,
                        'description': prop_details.get('description', ''),
                        'schema': prop_details
                    }
                    parameters.append(param_info)

        return parameters

    def extract_parameters(self, api_details: Dict[str, Any]) -> List[Dict[str, Any]]:
        """
        Extract and normalize parameter information from API details

        Args:
            api_details: API details from Swagger documentation

        Returns:
            List of normalized parameter information
        """
        parameters = []

        # Get the complete API docs for reference resolution
        # We need to extract service name from the API details or use a cached version
        api_docs = getattr(self, '_current_api_docs', {})

        # Extract path, query, and body parameters
        for param in api_details.get('parameters', []):
            param_schema = param.get('schema', {})

            # Handle $ref in parameter schema
            if '$ref' in param_schema:
                ref_path = param_schema['$ref']
                logger.debug(f"Found $ref in parameter {param.get('name', 'unknown')}: {ref_path}")

                resolved_schema = self._resolve_ref(ref_path, api_docs)
                if resolved_schema:
                    logger.debug(f"Successfully resolved $ref {ref_path}")

                    # Extract properties from the resolved schema
                    resolved_params = self._extract_properties_from_schema(resolved_schema, api_docs)

                    # Add the resolved parameters
                    parameters.extend(resolved_params)

                    # Also add the original parameter with resolved schema
                    param_info = {
                        'name': param.get('name', ''),
                        'type': resolved_schema.get('type', 'object'),
                        'in': param.get('in', 'body'),
                        'required': param.get('required', False),
                        'description': param.get('description', resolved_schema.get('description', '')),
                        'schema': resolved_schema
                    }
                    parameters.append(param_info)
                else:
                    logger.warning(f"Failed to resolve $ref {ref_path}")
                    # Fall back to original parameter info
                    param_info = {
                        'name': param.get('name', ''),
                        'type': param.get('type', 'string'),
                        'in': param.get('in', 'query'),
                        'required': param.get('required', False),
                        'description': param.get('description', ''),
                        'schema': param_schema
                    }
                    parameters.append(param_info)
            else:
                # Regular parameter without $ref
                param_info = {
                    'name': param.get('name', ''),
                    'type': param.get('type', 'string'),
                    'in': param.get('in', 'query'),
                    'required': param.get('required', False),
                    'description': param.get('description', ''),
                    'schema': param_schema
                }
                parameters.append(param_info)

        # Extract request body parameters
        request_body = api_details.get('requestBody', {})
        if request_body:
            content = request_body.get('content', {})
            for content_type, schema_info in content.items():
                schema = schema_info.get('schema', {})

                # Use the enhanced schema extraction method
                body_params = self._extract_properties_from_schema(schema, api_docs)
                parameters.extend(body_params)

        return parameters
